jnunemaker-mongomapper 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/.gitignore +1 -0
  2. data/History +17 -0
  3. data/README.rdoc +6 -3
  4. data/Rakefile +3 -2
  5. data/VERSION +1 -1
  6. data/bin/mmconsole +56 -0
  7. data/lib/mongomapper.rb +48 -17
  8. data/lib/mongomapper/associations.rb +31 -39
  9. data/lib/mongomapper/associations/base.rb +40 -22
  10. data/lib/mongomapper/associations/belongs_to_polymorphic_proxy.rb +33 -0
  11. data/lib/mongomapper/associations/belongs_to_proxy.rb +10 -14
  12. data/lib/mongomapper/associations/many_embedded_polymorphic_proxy.rb +34 -0
  13. data/lib/mongomapper/associations/{has_many_embedded_proxy.rb → many_embedded_proxy.rb} +5 -5
  14. data/lib/mongomapper/associations/many_proxy.rb +55 -0
  15. data/lib/mongomapper/associations/proxy.rb +21 -14
  16. data/lib/mongomapper/callbacks.rb +1 -1
  17. data/lib/mongomapper/document.rb +82 -59
  18. data/lib/mongomapper/embedded_document.rb +121 -130
  19. data/lib/mongomapper/finder_options.rb +21 -6
  20. data/lib/mongomapper/key.rb +5 -7
  21. data/lib/mongomapper/observing.rb +1 -41
  22. data/lib/mongomapper/pagination.rb +52 -0
  23. data/lib/mongomapper/rails_compatibility/document.rb +15 -0
  24. data/lib/mongomapper/rails_compatibility/embedded_document.rb +25 -0
  25. data/lib/mongomapper/serialization.rb +1 -1
  26. data/mongomapper.gemspec +62 -36
  27. data/test/NOTE_ON_TESTING +1 -0
  28. data/test/functional/test_associations.rb +485 -0
  29. data/test/{test_callbacks.rb → functional/test_callbacks.rb} +2 -1
  30. data/test/functional/test_document.rb +636 -0
  31. data/test/functional/test_pagination.rb +82 -0
  32. data/test/functional/test_rails_compatibility.rb +31 -0
  33. data/test/functional/test_validations.rb +172 -0
  34. data/test/models.rb +92 -0
  35. data/test/test_helper.rb +5 -0
  36. data/test/{serializers → unit/serializers}/test_json_serializer.rb +0 -0
  37. data/test/unit/test_association_base.rb +131 -0
  38. data/test/unit/test_document.rb +115 -0
  39. data/test/{test_embedded_document.rb → unit/test_embedded_document.rb} +158 -66
  40. data/test/{test_finder_options.rb → unit/test_finder_options.rb} +66 -0
  41. data/test/{test_key.rb → unit/test_key.rb} +13 -1
  42. data/test/unit/test_mongo_id.rb +35 -0
  43. data/test/{test_mongomapper.rb → unit/test_mongomapper.rb} +0 -0
  44. data/test/{test_observing.rb → unit/test_observing.rb} +0 -0
  45. data/test/unit/test_pagination.rb +113 -0
  46. data/test/unit/test_rails_compatibility.rb +34 -0
  47. data/test/{test_serializations.rb → unit/test_serializations.rb} +0 -2
  48. data/test/{test_validations.rb → unit/test_validations.rb} +0 -134
  49. metadata +68 -36
  50. data/lib/mongomapper/associations/has_many_proxy.rb +0 -28
  51. data/lib/mongomapper/associations/polymorphic_belongs_to_proxy.rb +0 -31
  52. data/lib/mongomapper/rails_compatibility.rb +0 -23
  53. data/test/test_associations.rb +0 -149
  54. data/test/test_document.rb +0 -944
  55. data/test/test_rails_compatibility.rb +0 -29
@@ -1,57 +1,104 @@
1
1
  require 'observer'
2
2
 
3
3
  module MongoMapper
4
- module EmbeddedDocument
5
- class NotImplemented < StandardError; end
6
-
4
+ module EmbeddedDocument
7
5
  def self.included(model)
8
6
  model.class_eval do
9
7
  extend ClassMethods
10
8
  include InstanceMethods
11
-
9
+
12
10
  extend Associations::ClassMethods
13
11
  include Associations::InstanceMethods
14
-
12
+
13
+ include RailsCompatibility::EmbeddedDocument
15
14
  include Validatable
16
15
  include Serialization
17
16
  end
18
17
  end
19
-
18
+
20
19
  module ClassMethods
20
+ def inherited(subclass)
21
+ (@subclasses ||= []) << subclass
22
+ end
23
+
24
+ def subclasses
25
+ @subclasses
26
+ end
27
+
21
28
  def keys
22
- @keys ||= HashWithIndifferentAccess.new
29
+ @keys ||= if parent = parent_model
30
+ parent.keys.dup
31
+ else
32
+ HashWithIndifferentAccess.new
33
+ end
23
34
  end
24
-
25
- def key(name, type, options={})
35
+
36
+ def key(name, type, options={})
26
37
  key = Key.new(name, type, options)
27
38
  keys[key.name] = key
39
+
40
+ create_accessors_for(key)
41
+ add_to_subclasses(name, type, options)
28
42
  apply_validations_for(key)
29
43
  create_indexes_for(key)
44
+
30
45
  key
31
46
  end
32
-
47
+
48
+ def create_accessors_for(key)
49
+ define_method(key.name) do
50
+ read_attribute(key.name)
51
+ end
52
+
53
+ define_method("#{key.name}_before_typecast") do
54
+ read_attribute_before_typecast(key.name)
55
+ end
56
+
57
+ define_method("#{key.name}=") do |value|
58
+ write_attribute(key.name, value)
59
+ end
60
+
61
+ define_method("#{key.name}?") do
62
+ read_attribute(key.name).present?
63
+ end
64
+ end
65
+
66
+ def add_to_subclasses(name, type, options)
67
+ return if subclasses.blank?
68
+
69
+ subclasses.each do |subclass|
70
+ subclass.key name, type, options
71
+ end
72
+ end
73
+
33
74
  def ensure_index(name_or_array, options={})
34
75
  keys_to_index = if name_or_array.is_a?(Array)
35
76
  name_or_array.map { |pair| [pair[0], pair[1]] }
36
77
  else
37
78
  name_or_array
38
79
  end
39
-
80
+
40
81
  collection.create_index(keys_to_index, options.delete(:unique))
41
82
  end
42
-
83
+
43
84
  def embeddable?
44
85
  !self.ancestors.include?(Document)
45
86
  end
46
-
87
+
88
+ def parent_model
89
+ if parent = ancestors[1]
90
+ parent if parent.ancestors.include?(EmbeddedDocument)
91
+ end
92
+ end
93
+
47
94
  private
48
95
  def create_indexes_for(key)
49
96
  ensure_index key.name if key.options[:index]
50
97
  end
51
-
98
+
52
99
  def apply_validations_for(key)
53
100
  attribute = key.name.to_sym
54
-
101
+
55
102
  if key.options[:required]
56
103
  validates_presence_of(attribute)
57
104
  end
@@ -59,16 +106,16 @@ module MongoMapper
59
106
  if key.options[:unique]
60
107
  validates_uniqueness_of(attribute)
61
108
  end
62
-
109
+
63
110
  if key.options[:numeric]
64
111
  number_options = key.type == Integer ? {:only_integer => true} : {}
65
112
  validates_numericality_of(attribute, number_options)
66
113
  end
67
-
114
+
68
115
  if key.options[:format]
69
116
  validates_format_of(attribute, :with => key.options[:format])
70
117
  end
71
-
118
+
72
119
  if key.options[:length]
73
120
  length_options = case key.options[:length]
74
121
  when Integer
@@ -81,147 +128,91 @@ module MongoMapper
81
128
  validates_length_of(attribute, length_options)
82
129
  end
83
130
  end
84
-
85
131
  end
86
-
132
+
87
133
  module InstanceMethods
88
134
  def initialize(attrs={})
89
135
  unless attrs.nil?
90
- initialize_associations(attrs)
136
+ self.class.associations.each_pair do |name, association|
137
+ if collection = attrs.delete(name)
138
+ send("#{association.name}=", collection)
139
+ end
140
+ end
141
+
91
142
  self.attributes = attrs
92
143
  end
93
144
  end
94
-
145
+
95
146
  def attributes=(attrs)
96
147
  return if attrs.blank?
97
- attrs.each_pair do |key_name, value|
98
- if writer?(key_name)
99
- write_attribute(key_name, value)
100
- else
101
- writer_method ="#{key_name}="
102
- self.send(writer_method, value) if respond_to?(writer_method)
103
- end
148
+ attrs.each_pair do |method, value|
149
+ self.send("#{method}=", value)
104
150
  end
105
151
  end
106
-
152
+
107
153
  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
154
+ returning HashWithIndifferentAccess.new do |attributes|
155
+ self.class.keys.each_pair do |name, key|
156
+ value = value_for_key(key)
157
+ attributes[name] = value unless value.nil?
158
+ end
159
+
160
+ attributes.merge!(embedded_association_attributes)
113
161
  end
114
162
  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)
128
- end
129
-
163
+
130
164
  def [](name)
131
165
  read_attribute(name)
132
166
  end
133
-
167
+
134
168
  def []=(name, value)
135
169
  write_attribute(name, value)
136
170
  end
137
-
138
- def method_missing(method, *args, &block)
139
- attribute = method.to_s
140
-
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
150
- end
151
-
171
+
152
172
  def ==(other)
153
173
  other.is_a?(self.class) && attributes == other.attributes
154
174
  end
155
-
175
+
156
176
  def inspect
157
- attributes_as_nice_string = defined_key_names.collect do |name|
177
+ attributes_as_nice_string = self.class.keys.keys.collect do |name|
158
178
  "#{name}: #{read_attribute(name)}"
159
179
  end.join(", ")
160
180
  "#<#{self.class} #{attributes_as_nice_string}>"
161
181
  end
162
-
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
177
- 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
-
193
- def defined_key(name)
194
- self.class.keys[name]
195
- end
196
-
197
- def defined_key_names
198
- self.class.keys.keys
199
- end
200
-
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
182
+
183
+ private
184
+ def value_for_key(key)
185
+ if key.native?
186
+ read_attribute(key.name)
187
+ else
188
+ embedded_document = read_attribute(key.name)
189
+ embedded_document && embedded_document.attributes
212
190
  end
213
191
  end
214
-
215
- attributes
216
- end
217
-
218
- def initialize_associations(attrs={})
219
- self.class.associations.each_pair do |name, association|
220
- if collection = attrs.delete(name)
221
- __send__("#{association.name}=", collection)
192
+
193
+ def read_attribute(name)
194
+ self.class.keys[name].get(instance_variable_get("@#{name}"))
195
+ end
196
+
197
+ def read_attribute_before_typecast(name)
198
+ instance_variable_get("@#{name}_before_typecast")
199
+ end
200
+
201
+ def write_attribute(name, value)
202
+ instance_variable_set "@#{name}_before_typecast", value
203
+ instance_variable_set "@#{name}", self.class.keys[name].set(value)
204
+ end
205
+
206
+ def embedded_association_attributes
207
+ returning HashWithIndifferentAccess.new do |attrs|
208
+ self.class.associations.each_pair do |name, association|
209
+ next unless association.embeddable?
210
+ next unless documents = instance_variable_get(association.ivar)
211
+
212
+ attrs[name] = documents.collect { |doc| doc.attributes }
213
+ end
222
214
  end
223
215
  end
224
- end
225
- end
226
- end
227
- end
216
+ end # InstanceMethods
217
+ end # EmbeddedDocument
218
+ end # MongoMapper
@@ -2,30 +2,45 @@ 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
+
12
+ dealing_with_ids = field.to_s == '_id' ||
13
+ (parent_key && parent_key.to_s == '_id')
14
+
15
+ criteria[field] = if dealing_with_ids
16
+ ids = value.map { |id| MongoID.mm_typecast(id) }
17
+ operator_present ? ids : {'$in' => ids}
18
+ elsif operator_present
19
+ value
20
+ else
21
+ {'$in' => value}
22
+ end
12
23
  when Hash
13
- criteria[field] = to_mongo_criteria(value)
24
+ criteria[field] = to_mongo_criteria(value, field)
14
25
  else
26
+ if field.to_s == '_id'
27
+ value = MongoID.mm_typecast(value)
28
+ end
29
+
15
30
  criteria[field] = value
16
31
  end
17
32
  end
18
33
 
19
34
  criteria
20
35
  end
21
-
36
+
22
37
  def self.to_mongo_options(options)
23
38
  options = options.dup
24
39
  {
25
40
  :fields => to_mongo_fields(options.delete(:fields) || options.delete(:select)),
26
41
  :offset => (options.delete(:offset) || 0).to_i,
27
42
  :limit => (options.delete(:limit) || 0).to_i,
28
- :sort => to_mongo_sort(options.delete(:order))
43
+ :sort => options.delete(:sort) || to_mongo_sort(options.delete(:order))
29
44
  }
30
45
  end
31
46
 
@@ -1,17 +1,14 @@
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, MongoID]
8
5
 
9
6
  attr_accessor :name, :type, :options, :default_value
10
7
 
11
8
  def initialize(name, type, options={})
12
9
  @name, @type = name.to_s, type
13
- self.options = options.symbolize_keys
14
- self.default_value = options.delete(:default)
10
+ self.options = (options || {}).symbolize_keys
11
+ self.default_value = self.options.delete(:default)
15
12
  end
16
13
 
17
14
  def ==(other)
@@ -50,8 +47,9 @@ module MongoMapper
50
47
  elsif type == Float then value.to_f
51
48
  elsif type == Array then value.to_a
52
49
  elsif type == Time then Time.parse(value.to_s)
50
+ elsif type == MongoID then MongoID.mm_typecast(value)
53
51
  #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 == Boolean then Boolean.mm_typecast(value)
55
53
  elsif type == Integer
56
54
  # ganked from datamapper
57
55
  value_to_i = value.to_i
@@ -7,39 +7,8 @@ module MongoMapper
7
7
  def self.included(model)
8
8
  model.class_eval do
9
9
  extend Observable
10
- extend ClassMethods
11
10
  end
12
11
  end
13
-
14
- module ClassMethods
15
- def observers=(*observers)
16
- @observers = observers.flatten
17
- end
18
-
19
- def observers
20
- @observers ||= []
21
- end
22
-
23
- def instantiate_observers
24
- return if @observers.blank?
25
- @observers.each do |observer|
26
- if observer.respond_to?(:to_sym) # Symbol or String
27
- observer.to_s.camelize.constantize.instance
28
- elsif observer.respond_to?(:instance)
29
- observer.instance
30
- else
31
- raise ArgumentError, "#{observer} must be a lowercase, underscored class name (or an instance of the class itself) responding to the instance method. Example: Person.observers = :big_brother # calls BigBrother.instance"
32
- end
33
- end
34
- end
35
-
36
- protected
37
- def inherited(subclass)
38
- super
39
- changed
40
- notify_observers :observed_class_inherited, subclass
41
- end
42
- end
43
12
  end
44
13
 
45
14
  class Observer
@@ -62,27 +31,18 @@ module MongoMapper
62
31
  end
63
32
 
64
33
  def initialize
65
- Set.new(observed_classes + observed_subclasses).each { |klass| add_observer! klass }
34
+ Set.new(observed_classes).each { |klass| add_observer! klass }
66
35
  end
67
36
 
68
37
  def update(observed_method, object) #:nodoc:
69
38
  send(observed_method, object) if respond_to?(observed_method)
70
39
  end
71
40
 
72
- def observed_class_inherited(subclass) #:nodoc:
73
- self.class.observe(observed_classes + [subclass])
74
- add_observer!(subclass)
75
- end
76
-
77
41
  protected
78
42
  def observed_classes
79
43
  Set.new([self.class.observed_class].compact.flatten)
80
44
  end
81
45
 
82
- def observed_subclasses
83
- observed_classes.sum([]) { |klass| klass.send(:subclasses) }
84
- end
85
-
86
46
  def add_observer!(klass)
87
47
  klass.add_observer(self)
88
48
  end