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,237 @@
1
+ require 'observer'
2
+
3
+ module MongoMapper
4
+ module EmbeddedDocument
5
+ def self.included(model)
6
+ model.class_eval do
7
+ extend ClassMethods
8
+ include InstanceMethods
9
+
10
+ extend Associations::ClassMethods
11
+ include Associations::InstanceMethods
12
+
13
+ include RailsCompatibility::EmbeddedDocument
14
+ include Validatable
15
+ include Serialization
16
+
17
+ key :_id, MongoID
18
+ end
19
+ end
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
+
34
+ def keys
35
+ @keys ||= if parent = parent_model
36
+ parent.keys.dup
37
+ else
38
+ HashWithIndifferentAccess.new
39
+ end
40
+ end
41
+
42
+ def key(name, type, options={})
43
+ key = Key.new(name, type, options)
44
+ keys[key.name] = key
45
+
46
+ create_accessors_for(key)
47
+ add_to_subclasses(name, type, options)
48
+ apply_validations_for(key)
49
+ create_indexes_for(key)
50
+
51
+ key
52
+ end
53
+
54
+ def create_accessors_for(key)
55
+ define_method(key.name) do
56
+ read_attribute(key.name)
57
+ end
58
+
59
+ define_method("#{key.name}_before_typecast") do
60
+ read_attribute_before_typecast(key.name)
61
+ end
62
+
63
+ define_method("#{key.name}=") do |value|
64
+ write_attribute(key.name, value)
65
+ end
66
+
67
+ define_method("#{key.name}?") do
68
+ read_attribute(key.name).present?
69
+ end
70
+ end
71
+
72
+ def add_to_subclasses(name, type, options)
73
+ return if subclasses.blank?
74
+
75
+ subclasses.each do |subclass|
76
+ subclass.key name, type, options
77
+ end
78
+ end
79
+
80
+ def ensure_index(name_or_array, options={})
81
+ keys_to_index = if name_or_array.is_a?(Array)
82
+ name_or_array.map { |pair| [pair[0], pair[1]] }
83
+ else
84
+ name_or_array
85
+ end
86
+
87
+ collection.create_index(keys_to_index, options.delete(:unique))
88
+ end
89
+
90
+ def embeddable?
91
+ !self.ancestors.include?(Document)
92
+ end
93
+
94
+ def parent_model
95
+ if parent = ancestors[1]
96
+ parent if parent.ancestors.include?(EmbeddedDocument)
97
+ end
98
+ end
99
+
100
+ private
101
+ def create_indexes_for(key)
102
+ ensure_index key.name if key.options[:index]
103
+ end
104
+
105
+ def apply_validations_for(key)
106
+ attribute = key.name.to_sym
107
+
108
+ if key.options[:required]
109
+ validates_presence_of(attribute)
110
+ end
111
+
112
+ if key.options[:unique]
113
+ validates_uniqueness_of(attribute)
114
+ end
115
+
116
+ if key.options[:numeric]
117
+ number_options = key.type == Integer ? {:only_integer => true} : {}
118
+ validates_numericality_of(attribute, number_options)
119
+ end
120
+
121
+ if key.options[:format]
122
+ validates_format_of(attribute, :with => key.options[:format])
123
+ end
124
+
125
+ if key.options[:length]
126
+ length_options = case key.options[:length]
127
+ when Integer
128
+ {:minimum => 0, :maximum => key.options[:length]}
129
+ when Range
130
+ {:within => key.options[:length]}
131
+ when Hash
132
+ key.options[:length]
133
+ end
134
+ validates_length_of(attribute, length_options)
135
+ end
136
+ end
137
+ end
138
+
139
+ module InstanceMethods
140
+ def initialize(attrs={})
141
+ unless attrs.nil?
142
+ self.class.associations.each_pair do |name, association|
143
+ if collection = attrs.delete(name)
144
+ send("#{association.name}=", collection)
145
+ end
146
+ end
147
+
148
+ self.attributes = attrs
149
+ end
150
+
151
+ if self.class.embeddable? && read_attribute(:_id).blank?
152
+ write_attribute :_id, XGen::Mongo::Driver::ObjectID.new
153
+ end
154
+ end
155
+
156
+ def attributes=(attrs)
157
+ return if attrs.blank?
158
+ attrs.each_pair do |method, value|
159
+ self.send("#{method}=", value)
160
+ end
161
+ end
162
+
163
+ def attributes
164
+ returning HashWithIndifferentAccess.new do |attributes|
165
+ self.class.keys.each_pair do |name, key|
166
+ value = value_for_key(key)
167
+ attributes[name] = value unless value.nil?
168
+ end
169
+
170
+ attributes.merge!(embedded_association_attributes)
171
+ end
172
+ end
173
+
174
+ def [](name)
175
+ read_attribute(name)
176
+ end
177
+
178
+ def []=(name, value)
179
+ write_attribute(name, value)
180
+ end
181
+
182
+ def ==(other)
183
+ other.is_a?(self.class) && id == other.id
184
+ end
185
+
186
+ def id
187
+ self._id.to_s
188
+ end
189
+
190
+ def id=(value)
191
+ self._id = value
192
+ end
193
+
194
+ def inspect
195
+ attributes_as_nice_string = self.class.keys.keys.collect do |name|
196
+ "#{name}: #{read_attribute(name)}"
197
+ end.join(", ")
198
+ "#<#{self.class} #{attributes_as_nice_string}>"
199
+ end
200
+
201
+ private
202
+ def value_for_key(key)
203
+ if key.native?
204
+ read_attribute(key.name)
205
+ else
206
+ embedded_document = read_attribute(key.name)
207
+ embedded_document && embedded_document.attributes
208
+ end
209
+ end
210
+
211
+ def read_attribute(name)
212
+ val = self.class.keys[name].get(instance_variable_get("@#{name}"))
213
+ instance_variable_set "@#{name}", val
214
+ end
215
+
216
+ def read_attribute_before_typecast(name)
217
+ instance_variable_get("@#{name}_before_typecast")
218
+ end
219
+
220
+ def write_attribute(name, value)
221
+ instance_variable_set "@#{name}_before_typecast", value
222
+ instance_variable_set "@#{name}", self.class.keys[name].set(value)
223
+ end
224
+
225
+ def embedded_association_attributes
226
+ returning HashWithIndifferentAccess.new do |attrs|
227
+ self.class.associations.each_pair do |name, association|
228
+ next unless association.embeddable?
229
+ next unless documents = instance_variable_get(association.ivar)
230
+
231
+ attrs[name] = documents.collect { |doc| doc.attributes }
232
+ end
233
+ end
234
+ end
235
+ end # InstanceMethods
236
+ end # EmbeddedDocument
237
+ end # MongoMapper
@@ -0,0 +1,96 @@
1
+ module MongoMapper
2
+ class FinderOptions
3
+ attr_reader :options
4
+
5
+ def self.to_mongo_criteria(conditions, parent_key=nil)
6
+ criteria = {}
7
+ conditions.each_pair do |field, value|
8
+ case value
9
+ when Array
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
23
+ when Hash
24
+ criteria[field] = to_mongo_criteria(value, field)
25
+ else
26
+ if field.to_s == '_id'
27
+ value = MongoID.mm_typecast(value)
28
+ end
29
+
30
+ criteria[field] = value
31
+ end
32
+ end
33
+
34
+ criteria
35
+ end
36
+
37
+ def self.to_mongo_options(options)
38
+ options = options.dup
39
+ {
40
+ :fields => to_mongo_fields(options.delete(:fields) || options.delete(:select)),
41
+ :offset => (options.delete(:offset) || 0).to_i,
42
+ :limit => (options.delete(:limit) || 0).to_i,
43
+ :sort => options.delete(:sort) || to_mongo_sort(options.delete(:order))
44
+ }
45
+ end
46
+
47
+ def initialize(options)
48
+ raise ArgumentError, "FinderOptions must be a hash" unless options.is_a?(Hash)
49
+ @options = options.symbolize_keys
50
+ @conditions = @options.delete(:conditions) || {}
51
+ end
52
+
53
+ def criteria
54
+ self.class.to_mongo_criteria(@conditions)
55
+ end
56
+
57
+ def options
58
+ self.class.to_mongo_options(@options)
59
+ end
60
+
61
+ def to_a
62
+ [criteria, options]
63
+ end
64
+
65
+ private
66
+ def self.to_mongo_fields(fields)
67
+ return if fields.blank?
68
+
69
+ if fields.is_a?(String)
70
+ fields.split(',').map { |field| field.strip }
71
+ else
72
+ fields.flatten.compact
73
+ end
74
+ end
75
+
76
+ def self.to_mongo_sort(sort)
77
+ return if sort.blank?
78
+ pieces = sort.split(',')
79
+ pairs = pieces.map { |s| to_mongo_sort_piece(s) }
80
+
81
+ hash = OrderedHash.new
82
+ pairs.each do |pair|
83
+ field, sort_direction = pair
84
+ hash[field] = sort_direction
85
+ end
86
+ hash.symbolize_keys
87
+ end
88
+
89
+ def self.to_mongo_sort_piece(str)
90
+ field, direction = str.strip.split(' ')
91
+ direction ||= 'ASC'
92
+ direction = direction.upcase == 'ASC' ? 1 : -1
93
+ [field, direction]
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,80 @@
1
+ module MongoMapper
2
+ class Key
3
+ # DateTime and Date are currently not supported by mongo's bson so just use Time
4
+ NativeTypes = [String, Float, Time, Integer, Boolean, Array, Hash, MongoID]
5
+
6
+ attr_accessor :name, :type, :options, :default_value
7
+
8
+ def initialize(name, type, options={})
9
+ @name, @type = name.to_s, type
10
+ self.options = (options || {}).symbolize_keys
11
+ self.default_value = self.options.delete(:default)
12
+ end
13
+
14
+ def ==(other)
15
+ @name == other.name && @type == other.type
16
+ end
17
+
18
+ def set(value)
19
+ typecast(value)
20
+ end
21
+
22
+ def native?
23
+ @native ||= NativeTypes.include?(type)
24
+ end
25
+
26
+ def embedded_document?
27
+ type.ancestors.include?(EmbeddedDocument) && !type.ancestors.include?(Document)
28
+ end
29
+
30
+ def get(value)
31
+ return default_value if value.nil? && !default_value.nil?
32
+ if type == Array
33
+ value || []
34
+ elsif type == Hash
35
+ HashWithIndifferentAccess.new(value || {})
36
+ else
37
+ value
38
+ end
39
+ end
40
+
41
+ private
42
+ def typecast(value)
43
+ return HashWithIndifferentAccess.new(value) if value.is_a?(Hash) && type == Hash
44
+ return value if value.kind_of?(type) || value.nil?
45
+ begin
46
+ if type == String then value.to_s
47
+ elsif type == Float then value.to_f
48
+ elsif type == Array then value.to_a
49
+ elsif type == Time then Time.parse(value.to_s)
50
+ elsif type == MongoID then MongoID.mm_typecast(value)
51
+ #elsif type == Date then Date.parse(value.to_s)
52
+ elsif type == Boolean then Boolean.mm_typecast(value)
53
+ elsif type == Integer
54
+ # ganked from datamapper
55
+ value_to_i = value.to_i
56
+ if value_to_i == 0 && value != '0'
57
+ value_to_s = value.to_s
58
+ begin
59
+ Integer(value_to_s =~ /^(\d+)/ ? $1 : value_to_s)
60
+ rescue ArgumentError
61
+ nil
62
+ end
63
+ else
64
+ value_to_i
65
+ end
66
+ elsif embedded_document?
67
+ typecast_embedded_document(value)
68
+ else
69
+ value
70
+ end
71
+ rescue
72
+ value
73
+ end
74
+ end
75
+
76
+ def typecast_embedded_document(value)
77
+ value.is_a?(type) ? value : type.new(value)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,50 @@
1
+ require 'observer'
2
+ require 'singleton'
3
+ require 'set'
4
+
5
+ module MongoMapper
6
+ module Observing #:nodoc:
7
+ def self.included(model)
8
+ model.class_eval do
9
+ extend Observable
10
+ end
11
+ end
12
+ end
13
+
14
+ class Observer
15
+ include Singleton
16
+
17
+ class << self
18
+ def observe(*models)
19
+ models.flatten!
20
+ models.collect! { |model| model.is_a?(Symbol) ? model.to_s.camelize.constantize : model }
21
+ define_method(:observed_classes) { Set.new(models) }
22
+ end
23
+
24
+ def observed_class
25
+ if observed_class_name = name[/(.*)Observer/, 1]
26
+ observed_class_name.constantize
27
+ else
28
+ nil
29
+ end
30
+ end
31
+ end
32
+
33
+ def initialize
34
+ Set.new(observed_classes).each { |klass| add_observer! klass }
35
+ end
36
+
37
+ def update(observed_method, object) #:nodoc:
38
+ send(observed_method, object) if respond_to?(observed_method)
39
+ end
40
+
41
+ protected
42
+ def observed_classes
43
+ Set.new([self.class.observed_class].compact.flatten)
44
+ end
45
+
46
+ def add_observer!(klass)
47
+ klass.add_observer(self)
48
+ end
49
+ end
50
+ end