djsun-mongomapper 0.3.1

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